/*******************************************************************************
 * Copyright (c) 2003, 2005 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package com.architexa.org.eclipse.gef;

import com.architexa.org.eclipse.draw2d.geometry.PrecisionRectangle;
import com.architexa.org.eclipse.gef.requests.GroupRequest;
import com.architexa.org.eclipse.gef.rulers.RulerProvider;

import java.util.Map;



/**
 * A helper used to perform snapping to guides. The guides are obtained from the viewer's
 * horizontal and vertical {@link RulerProvider RulerProviders}.  If snapping is
 * performed, the request's extended data will contain keyed values indicating which
 * guides were snapped to, and which side of the part should be attached.  Generally
 * snapping to a guide should attach the part to that guide, but application behavior may
 * differ.
 * <P>
 * Snapping (and attaching) to a guide is only possible if a single part is being dragged.
 * The current implementation will not snap if a request contains multiple parts. This may
 * be relaxed in the future to allow snapping, but without setting the attachment extended
 * data.
 * <P>
 * This helper does not keep up with changes in guides.  Clients should instantiate a new
 * helper each time one is requested and not hold on to instances of the helper.
 * 
 * @since 3.0
 * @author Randy Hudson
 * @author Pratik Shah
 */
public class SnapToGuides 
	extends SnapToHelper
{

/**
 * The key used to identify the Vertical Guide. This key is used with the request's
 * extended data map to store an Integer.  The integer value is the location of the guide
 * that is being snapped to.
 */
public static final String KEY_VERTICAL_GUIDE = "SnapToGuides.VerticalGuide"; //$NON-NLS-1$

/**
 * The key used to identify the Horizontal Guide. This key is used with the request's
 * extended data map to store an Integer.  The integer value is the location of the guide
 * that is being snapped to.
 */
public static final String KEY_HORIZONTAL_GUIDE = "SnapToGuides.HorizontalGuide"; //$NON-NLS-1$

/**
 * The key used to identify the vertical anchor point.  This key is used with the
 * request's extended data map to store an Integer.  If the VERTICAL_GUIDE has been set,
 * then this integer is a number identifying which side of the dragged object is being
 * snapped to that guide.
 * <UL>
 *   <LI><code>-1</code> indicates the left side should be attached.
 *   <LI><code> 0</code> indicates the center should be attached.
 *   <LI><code> 1</code> indicates the right side should be attached.
 * </UL>
 */
public static final String KEY_VERTICAL_ANCHOR = "SnapToGuides.VerticalAttachment"; //$NON-NLS-1$

/**
 * The key used to identify the horizontal anchor point.  This key is used with the
 * request's extended data map to store an Integer.  If the HORIZONTAL_GUIDE has been set,
 * then this integer is a number identifying which side of the dragged object is being
 * snapped to that guide.
 * <UL>
 *   <LI><code>-1</code> indicates the top side should be attached.
 *   <LI><code> 0</code> indicates the middle should be attached.
 *   <LI><code> 1</code> indicates the bottom side should be attached.
 * </UL>
 */
public static final String KEY_HORIZONTAL_ANCHOR = "SnapToGuides.HorizontalAttachment";//$NON-NLS-1$

/**
 * The threshold for snapping to guides.  The rectangle being snapped must be within +/-
 * the THRESHOLD.  The default value is 5.001;
 */
protected static final double THRESHOLD = 5.001;

/**
 * The graphical editpart to which guides are relative.  This should also the parent of
 * the parts being snapped to guides.
 */
protected GraphicalEditPart container;

/**
 * The locations of the vertical guides in the container's coordinates.  Use {@link
 * #getVerticalGuides()}.
 */
protected int[] verticalGuides;

/**
 * The locations of the horizontal guides in the container's coordinates. Use {@link
 * #getHorizontalGuides()}.
 */
protected int[] horizontalGuides;

/**
 * Constructs a new snap-to-guides helper using the given container as the basis. 
 * @param container the container editpart
 */
public SnapToGuides(GraphicalEditPart container) {
	this.container = container;
}

/**
 * Returns the correction for the given near and far sides of a rectangle or {@link
 * #THRESHOLD} if no correction was found.  The near side represents the top or left side
 * of a rectangle being snapped.  Similar for far.  If snapping occurs, the extendedData
 * will have the guide and attachment point set.
 * 
 * @param guides the location of the guides
 * @param near the top or left location
 * @param far the bottom or right location
 * @param extendedData the map for storing snap details
 * @param isVertical <code>true</code> if for vertical guides, <code>false</code>
 * for horizontal.
 * @return the correction amount or THRESHOLD if no correction was made
 */
protected double getCorrectionFor(int[] guides, double near, double far, Map extendedData, 
                                  boolean isVertical) {
	far -= 1.0;
	double total = near + far;
	//If the width is even, there is no middle pixel so favor the left - most pixel.
	if ((int)(near - far) % 2 == 0)
		total -= 1.0;
	double result = getCorrectionFor(guides, total / 2, extendedData, isVertical, 0);
	if (result == THRESHOLD)
		result = getCorrectionFor(guides, near, extendedData, isVertical, -1); 
	if (result == THRESHOLD)
		result = getCorrectionFor(guides, far, extendedData, isVertical, 1);
	return result;
}

/**
 * Returns the correction for the given location or {@link #THRESHOLD} if no correction
 * was found. If correction occurs, the extendedData will have the guide and attachment
 * point set. The attachment point is identified by the <code>side</code> parameter.
 * <P>
 * The correction's magnitude will be less than THRESHOLD.
 * 
 * @param guides the location of the guides
 * @param value the location being tested
 * @param extendedData the map for storing snap details
 * @param vert <code>true</code> if for vertical guides, <code>false</code>
 * @param side the integer indicating which side is being snapped
 * @return a correction amount or THRESHOLD if no correction was made
 */
protected double getCorrectionFor(int[] guides, double value, Map extendedData, 
                                  boolean vert, int side) {
	double resultMag = THRESHOLD;
	double result = THRESHOLD;
	
	for (int i = 0; i < guides.length; i++) {
		int offset = guides[i];
		double magnitude;
		
		magnitude = Math.abs(value - offset);
		if (magnitude < resultMag) {
			extendedData.put(vert ? KEY_VERTICAL_GUIDE : KEY_HORIZONTAL_GUIDE, 
					new Integer(guides[i]));
			extendedData.put(vert ? KEY_VERTICAL_ANCHOR : KEY_HORIZONTAL_ANCHOR, 
					new Integer(side));
			resultMag = magnitude;
			result = offset - value;
		}
	}
	return result;
}

/**
 * Returns the horizontal guides in the coordinates of the container's contents pane.
 * @return the horizontal guides 
 */
protected int[] getHorizontalGuides() {	
	if (horizontalGuides == null) {
		RulerProvider rProvider = ((RulerProvider)container.getViewer()
					.getProperty(RulerProvider.PROPERTY_VERTICAL_RULER));
		if (rProvider != null)
			horizontalGuides = rProvider.getGuidePositions();	
		else
			horizontalGuides = new int[0];
	}
	return horizontalGuides;
}

/**
 * Returns the vertical guides in the coordinates of the container's contents pane.
 * @return the vertical guides 
 */
protected int[] getVerticalGuides() {
	if (verticalGuides == null) {
		RulerProvider rProvider = ((RulerProvider)container.getViewer()
				.getProperty(RulerProvider.PROPERTY_HORIZONTAL_RULER));
		if (rProvider != null)
			verticalGuides = rProvider.getGuidePositions();	
		else
			verticalGuides = new int[0];
	}
	return verticalGuides;
}

/**
 * @see SnapToHelper#snapRectangle(Request, int, PrecisionRectangle, PrecisionRectangle)
 */
public int snapRectangle(Request request, int snapOrientation,
		PrecisionRectangle baseRect, PrecisionRectangle result) {
	if (request instanceof GroupRequest 
			&& ((GroupRequest)request).getEditParts().size() > 1)
		return snapOrientation;
	
	baseRect = baseRect.getPreciseCopy();
	makeRelative(container.getContentPane(), baseRect);
	PrecisionRectangle correction = new PrecisionRectangle();
	makeRelative(container.getContentPane(), correction);

	if ((snapOrientation & HORIZONTAL) != 0) {
		double xcorrect = getCorrectionFor(getVerticalGuides(), baseRect.preciseX,
				baseRect.preciseRight(), request.getExtendedData(), true);
		if (xcorrect != THRESHOLD) {
			snapOrientation &= ~HORIZONTAL;
			correction.preciseX += xcorrect;
		}
	}
	
	if ((snapOrientation & VERTICAL) != 0) {
		double ycorrect = getCorrectionFor(getHorizontalGuides(), baseRect.preciseY,
				baseRect.preciseBottom(), request.getExtendedData(), false);
		if (ycorrect != THRESHOLD) {
			snapOrientation &= ~VERTICAL;
			correction.preciseY += ycorrect;
		}
	}
	
	boolean snapped = false;
	if (!snapped && (snapOrientation & WEST) != 0) {
		double leftCorrection = getCorrectionFor(getVerticalGuides(), 
				baseRect.preciseX, request.getExtendedData(), true, -1);
		if (leftCorrection != THRESHOLD) {
			snapOrientation &= ~WEST;
			correction.preciseWidth -= leftCorrection;
			correction.preciseX += leftCorrection;
		}
	}

	if (!snapped && (snapOrientation & EAST) != 0) {
		double rightCorrection = getCorrectionFor(getVerticalGuides(), 
				baseRect.preciseRight() - 1, request.getExtendedData(), true, 1);
		if (rightCorrection != THRESHOLD) {
			snapped = true;
			snapOrientation &= ~EAST;
			correction.preciseWidth += rightCorrection;
		}
	}
	
	snapped = false;
	if (!snapped && (snapOrientation & NORTH) != 0) {	
		double topCorrection = getCorrectionFor(getHorizontalGuides(), 
				baseRect.preciseY, request.getExtendedData(), false, -1);
		if (topCorrection != THRESHOLD) {
			snapOrientation &= ~NORTH;
			correction.preciseHeight -= topCorrection;
			correction.preciseY += topCorrection;
		}
	}

	if (!snapped && (snapOrientation & SOUTH) != 0) {
		double bottom = getCorrectionFor(getHorizontalGuides(), 
				baseRect.preciseBottom() - 1, request.getExtendedData(), false, 1);
		if (bottom != THRESHOLD) {
			snapped = true;
			snapOrientation &= ~SOUTH;
			correction.preciseHeight += bottom;
		}
	}

	correction.updateInts();
	makeAbsolute(container.getContentPane(), correction);
	result.preciseX += correction.preciseX;
	result.preciseY += correction.preciseY;
	result.preciseWidth += correction.preciseWidth;
	result.preciseHeight += correction.preciseHeight;
	result.updateInts();
	
	return snapOrientation;
}

}
